package com.hup.utils.commons.start;

import com.hup.utils.commons.exception.AppCommonException;
import com.hup.utils.commons.thread.ThreadUtil;
import lombok.extern.log4j.Log4j2;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.concurrent.Executors;

/**
 * 通过启动时占用[DUP]端口号,实现单例App的功能;
 * TCP端口号被占用是不影响的
 *
 * @author hugan
 * @date 2020/4/7
 */
@Log4j2
public class SingletonAppManager {

    /**
     * 请求获取appTag的消息
     */
    private static final String REQ_GET_APP_TAG = "isMyApp";
    private DatagramSocket socket;
    private boolean canRun;

    /**
     * <pre>
     * 创建单例AppManager;
     * 成功返回时,就代表启动单例成功;(不重写下面方法的前提下)
     * 否则成功返回前会触发
     *    {@link SingletonAppHandler#onPortUsedByMyApp()},
     *    或 {@link SingletonAppHandler#onPortUsedByOtherApp()};
     * 调用时机:一般在MainPresenter构造时就调用(失败时,可以避免创建其他Component)
     * </pre>
     *
     * @return 启动成功后返回的 Manager
     */
    public static SingletonAppManager doStart(SingletonAppHandler handler) {
        if (handler.port() <= 0 || handler.port() > 65535) {
            throw new IllegalArgumentException("单例端口号取值错误:" + handler.port());
        }
        return new SingletonAppManager(handler);
    }

    private SingletonAppManager(SingletonAppHandler handler) {
        try {
            socket = new DatagramSocket(handler.port());
            log.info("单例socket创建成功={}", handler.port());
        } catch (Exception e) {
            socket = null;
            log.warn("单例socket创建失败");
        }
        if (socket != null) {
            startReadingThread(socket, handler);
        } else {
            //已经有程序占用了该端口
            boolean isMyApp = isMyApp(handler);
            if (isMyApp) handler.onPortUsedByMyApp();
            else handler.onPortUsedByOtherApp();
        }
    }

    private void startReadingThread(DatagramSocket socket, SingletonAppHandler handler) {
        new Thread(() -> {
            Thread.currentThread().setName("SingletonAppManager-[" + handler.port() + "]receiver");
            log.debug("开启单例守护线程");
            byte[] inputBuff = new byte[512];
            DatagramPacket inputPacket = new DatagramPacket(inputBuff, inputBuff.length);
            try {
                canRun = true;
                while (canRun) {
                    inputPacket.setData(inputBuff);
                    socket.receive(inputPacket);

                    int respPort = inputPacket.getPort();
                    int readLength = inputPacket.getLength();
                    handler.debugReceivedMsg(respPort, readLength, inputBuff);
                    log.info("收到消息:respPort={}, readLength={}", respPort, readLength);
                    String receivedMsg = null;
                    if (readLength != 0) {
                        byte[] data = new byte[readLength];
                        System.arraycopy(inputBuff, 0, data, 0, readLength);
                        receivedMsg = new String(data);
                    }
                    if (REQ_GET_APP_TAG.equals(receivedMsg)) {
                        //回复appTag
                        byte[] data = handler.appTag().getBytes();
                        socket.send(new DatagramPacket(data, data.length, inputPacket.getAddress(), respPort));
                        handler.onAppReduplicateStart();
                    }
                }
            } catch (Exception e) {
                if (canRun) {
                    handler.onManagerThreadException("单例守护线程异常,", e);
                    canRun = false;
                } else {
                    log.debug("正常退出线程");
                }
            } finally {
                closeSilent(socket);
            }
        }).start();
    }

    /**
     * 给该端口发送消息,并等待返回结果,如果返回不是预料的数据,则代表别的程序占用了端口
     */
    private boolean isMyApp(SingletonAppHandler handler) {
        DatagramSocket socket = null;
        try {
            Thread.currentThread().setName("SingletonAppManager-[" + handler.port() + "]receiver");
            socket = new DatagramSocket();//占用随机端口号
            socket.setSoTimeout(1000);
            byte[] inputBuff = new byte[512];
            DatagramPacket inputPacket = new DatagramPacket(inputBuff, inputBuff.length);
            /*
            步骤:
            启动接收线程
            新线程 发送消息
            等待接收消息,n秒内无正确返回值,则判断为[notMyApp]
             */
            DatagramSocket finalSocket = socket;
            Executors.newSingleThreadExecutor().execute(() -> {
                try {
                    Thread.currentThread().setName("SingletonAppManager-sender");
                    ThreadUtil.sleep(10);//防止发送时间早于,接受线程的socket.receive(inputPacket);
                    log.info("向旧实例[" + handler.port() + "]发送消息={}", REQ_GET_APP_TAG);
                    InetAddress address = InetAddress.getLocalHost();
                    byte[] data = REQ_GET_APP_TAG.getBytes();
                    finalSocket.send(new DatagramPacket(data, data.length, address, handler.port()));
                } catch (Exception e) {
                    handler.onUnReachableException("向[" + handler.port() + "]端口发送消息异常,", e);
                }
            });
            while (true) {
                log.info("等待端口[" + handler.port() + "]返回消息");
                inputPacket.setData(inputBuff);
                socket.receive(inputPacket);

                int respPort = inputPacket.getPort();
                int readLength = inputPacket.getLength();
                handler.debugReceivedMsg(respPort, readLength, inputBuff);
                log.info("收到消息:respPort={}, readLength={}", respPort, readLength);
                if (handler.port() != respPort) {
                    log.debug("收到其他端口号的消息");
                    //继续循环
                    continue;
                }
                String receivedMsg = null;
                if (readLength != 0) {
                    byte[] data = new byte[readLength];
                    System.arraycopy(inputBuff, 0, data, 0, readLength);
                    receivedMsg = new String(data);
                }
                //收到消息后,不论数据是什么,都会有结果了
                return handler.appTag().equals(receivedMsg);
            }
        } catch (SocketTimeoutException e) {
            log.debug("超时,仍未收到正确的数据");
            return false;
        } catch (Exception e) {
            handler.onUnReachableException("接收[" + handler.port() + "]端口的消息异常,", e);
            //和消息发送异常的情况一样,当作是[超时]
            return false;
        } finally {
            closeSilent(socket);
        }
    }

    private void closeSilent(DatagramSocket socket) {
        if (socket != null) {
            try {
                socket.close();
            } catch (Exception e) {
                //do nothing
            }
        }
    }

    public void doStop() {
        canRun = false;
        closeSilent(socket);
    }

    public interface SingletonAppHandler {

        /**
         * @return 要持有的UDP端口号, 建议 9000~65535
         */
        int port();

        /**
         * @return 本程序的唯一标识
         */
        String appTag();

        /**
         * 检测到有新程序实例启动时的操作
         */
        void onAppReduplicateStart();

        /**
         * @param desc 单例守护线程异常
         */
        default void onManagerThreadException(String desc, Exception e) {
            DialogHelper.show(desc, e, null);
        }

        /**
         * 本程序旧的实例,占用端口号:退出当前实例(已经通过onAppReStart()通知旧实例了);
         * 一般不需要重写(来释放资源),因为一般需要在确定能启动后才能让程序申请资源
         */
        default void onPortUsedByMyApp() {
            log.info("程序已有旧实例,即将退出本实例");
            //代码详解,详见[System.exit笔记.txt]
            new Thread(() -> System.exit(0)).start();//实现正常exit,避免ShutdownHook死锁
            throw new AppCommonException("程序已有旧实例,即将退出本实例");//终止SpringBoot继续注册component
        }

        /**
         * 修改port的文件的描述
         *
         * @return 如:[config/XXConfig.config的appPort],[config/application.properties的appPort]
         */
        String portSettingFileDesc();

        /**
         * 抛出异常
         */
        default void onPortUsedByOtherApp() {
            String configPath = portSettingFileDesc() == null ? "" : "在[" + portSettingFileDesc() + "]";
            throw new AppCommonException("单例端口号[" + port() + "]被其他程序占用,\n请" + configPath + "修改App的单例端口号!");
        }

        /**
         * 极少机率或不会发生的异常
         */
        default void onUnReachableException(String desc, Exception e) {
            log.warn("{} {}", desc, e.getMessage());
        }

        /**
         * 收到来自[respPort]端口的消息,用于测试
         */
        default void debugReceivedMsg(int respPort, int readLength, byte[] inputBuff) {}
    }
}
